﻿using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.IO;
using System.Text;
using System.Threading;
using CFProxy.FilterCode;

/*
 * Christiaan Rakowski
 * CFProxy - Content Filtering Proxy
 */

namespace CFProxy.Networking
{
	class ProxyServer
	{
		private HttpListener listener;
		private static StreamWriter log = new StreamWriter(String.Format("CFProxy-{0}.log", DateTime.Now.ToShortDateString()), true);
		//public static string[] Separators = new string[] { " ", ",", ".", "/", "\\", "|", "<", ">", "?", ";", ":", "[", "]", "{", "}", "=", "+", "-", "_", "(", ")", "*", "&", "^", "%", "$", "#", "@", "!", "~", "`" };
		public static FilterDatabase FilterDB;

		public ProxyServer()
		{
			FilterDB = new FilterDatabase(AppendLog);
			listener = new HttpListener();
		}

		public void Start()
		{
			try
			{
				AppendLog("Starting New Server");
				listener.Prefixes.Add(string.Format("http://*:{0}/", Settings.Port));
				listener.Start();
				Run();
			}
			catch (Exception e)
			{
				Stop();
				AppendLog(e.Message);
				throw e;
			}
		}

		private void Run()
		{
			try
			{
				while (true)
				{
					HttpListenerContext context = listener.GetContext();
					HandleHTTP client = new HandleHTTP(context, AppendLog);
					Thread t = new Thread(new ThreadStart(client.StartHandling));
					AppendLog("New connection accepted");
					t.Start();
				}
			}
			catch (Exception e)
			{
				throw e;
			}
		}

		public void Stop()
		{
			AppendLog("Stopping Server");
			listener.Stop();
		}

		public delegate void AppendLogDelegate(string logMessage);
		public void AppendLog(string logMessage)
		{
			log.WriteLine("{0}: {1}", DateTime.Now, logMessage);
			Console.WriteLine("{0}: {1}", DateTime.Now, logMessage);
		}
	}
	
	class HandleHTTP
	{
		private byte[] Buffer = new byte[3000];
		
		private HttpListenerContext context;
		private ProxyServer.AppendLogDelegate appendLog;

		public HandleHTTP(HttpListenerContext context, ProxyServer.AppendLogDelegate appendLog)
		{
			this.context = context;
			this.appendLog = appendLog;
		}

		/// <summary>
		/// Handle the clients request
		/// </summary>
		private void proxyRequest()
		{
			try
			{
				#region Receive
				if (FilterHost(context.Request.UserHostName))
				{
					context.Response.Close();
					return;
				}
				#endregion

				#region Resolve
				IPHostEntry hostEntry = Dns.GetHostEntry(context.Request.UserHostName);	//Get the DNS Entry
				IPAddress[] ip = hostEntry.AddressList;									//Get the IPAddresses
				#endregion

				#region Request
				appendLog(String.Format("Connecting To Target {0}", ip[0]));
				TcpClient tcpClient = new TcpClient(ip[0].ToString(), 80);
				byte[] data = Encoding.UTF8.GetBytes(String.Format("GET {0} HTTP/1.1\r\nHost: {1}\r\nConnection: close\r\n\r\n", context.Request.Url.PathAndQuery, context.Request.UserHostName));
				tcpClient.GetStream().Write(data, 0, data.Length);
				#endregion

				if (tcpClient.GetStream().CanRead)
				{
					#region Response
					byte[] buffer = new byte[32768];
					int read = 0;
					string responseString = string.Empty;

					while ((read = tcpClient.GetStream().Read(buffer, 0, buffer.Length)) > 0)
					{
#if DEBUG
						Console.WriteLine("Read {0} bytes", read);
#endif
						responseString += Encoding.UTF8.GetString(buffer, 0, read);
					}
					
					// Page Moved, request the new location
					while(	responseString.StartsWith("HTTP/1.1 301") ||
							responseString.StartsWith("HTTP/1.1 302") ||
							responseString.StartsWith("HTTP/1.1 303") )
					{
						#region ParseHeader
						int index = responseString.IndexOf("Location: ");
						index += "Location: ".Length;
						string url = responseString.Substring(index);
						index = url.IndexOf("\r\n");
						url = url.Substring(0, index);
						#endregion

						#region Re-Request
						responseString = string.Empty;
						WebRequest req = HttpWebRequest.Create(url);
						WebResponse res = req.GetResponse();
						Stream s = res.GetResponseStream();
						#endregion

						#region ReceiveResponse
						while ((read = s.Read(buffer, 0, buffer.Length)) > 0)
						{
							buffer.Initialize();
#if DEBUG
							Console.WriteLine("Read {0} bytes", read);
#endif
							responseString += Encoding.UTF8.GetString(buffer, 0, read);
						}
						#endregion
					}

					// Filter On Page Content
					if (FilterMessage(responseString))
					{
						appendLog(String.Format("Connection to host \"{0}\" blocked", context.Request.UserHostName));
						responseString = Settings.ErrorPage;
						
						//context.Response.Close();
						//tcpClient.Close();
						//return;
					}

					// Remove headers
					if (responseString.IndexOf("HTTP/1.1 200 OK") > -1)
					{
						responseString = responseString.Substring(responseString.IndexOf("\r\n\r\n"));
					}
					#endregion

					#region Relay
					byte[] byteArray = Encoding.UTF8.GetBytes(responseString);
					context.Response.OutputStream.Write(byteArray, 0, byteArray.Length);

					context.Response.OutputStream.Close();
					tcpClient.Close();
					#endregion
				}				
			}
			#region ErrorCatching
			catch (Exception ex)
			{
				Console.WriteLine(ex.Message);
			}
			#endregion
		}

		private void proxyRequestDifferently()
		{
			try
			{
				#region RequestResponse
				string response = GetResponse(RequestPage(context.Request.UserHostName));
				#endregion

				#region Relay
				byte[] byteArray = Encoding.UTF8.GetBytes(response);
				context.Response.OutputStream.Write(byteArray, 0, byteArray.Length);
				context.Response.OutputStream.Close();
				#endregion
			}
			#region ErrorCatching
			catch (Exception ex)
			{
				Console.WriteLine(ex.Message);
			}
			#endregion
		}

		private Stream RequestPage(string hostname)
		{
			#region Receive
			if (FilterHost(hostname))
			{
				appendLog(String.Format("Host \"{0}\" is blacklisted, Connection blocked", hostname));
				context.Response.Close();
				return null;
			}
			#endregion

			#region Resolve
			IPHostEntry hostEntry = Dns.GetHostEntry(hostname);
			IPAddress[] ip = hostEntry.AddressList;
			#endregion
			
			#region Request
			appendLog(String.Format("Connecting To Target {0}", ip[0]));
			TcpClient tcpClient = new TcpClient(ip[0].ToString(), 80);
			byte[] data = Encoding.UTF8.GetBytes(String.Format("GET {0} HTTP/1.1\r\nHost: {1}\r\nConnection: close\r\n\r\n", context.Request.Url.PathAndQuery, hostname));
			tcpClient.GetStream().Write(data, 0, data.Length);
			#endregion

			return tcpClient.GetStream();
		}

		private String GetResponse(Stream requestPageStream)
		{
			string responseString = string.Empty;
			if (requestPageStream.CanRead)
			{
				#region Response
				byte[] buffer = new byte[32768];
				int read = 0;

				while ((read = requestPageStream.Read(buffer, 0, buffer.Length)) > 0)
				{
#if DEBUG
					Console.WriteLine("Read {0} bytes", read);
#endif
					responseString += Encoding.UTF8.GetString(buffer, 0, read);
				}
				#endregion

				#region Re-Request
				// Page Moved, Solve recursivly
				if (responseString.StartsWith("HTTP/1.1 302"))
				{
					#region ParseHeader
					int index = responseString.IndexOf("Location: ");
					index += "Location: ".Length;
					string url = responseString.Substring(index);
					index = url.IndexOf("\r\n");
					url = url.Substring(0, index);
					#endregion

					return GetResponse(RequestPage(url));
				}
				#endregion

				#region Filter
				// Filter On Page Content
				if (FilterMessage(responseString))
				{
					appendLog(String.Format("Connection to host \"{0}\" blocked", context.Request.UserHostName));
					context.Response.Close();
					requestPageStream.Close();
					return "";
				}
				#endregion

				#region RemoveHeaders
				// Remove headers
				if (responseString.IndexOf("HTTP/1.1 200 OK") > -1)
				{
					responseString = responseString.Substring(responseString.IndexOf("\r\n\r\n"));
				}
				#endregion
			}
			return responseString;
		}

		/// <summary>
		/// Start handeling the clients request
		/// </summary>
		public void StartHandling()
		{
			proxyRequest();
		}

		/// <summary>
		/// Check if the message contains any filtered keywords
		/// If the keyword is only a substring of one of the keywords in one of the filters it will return as a hit.
		/// </summary>
		/// <param name="message">The message</param>
		/// <returns>True if a keyword was found, False if there was not</returns>
		private bool FilterMessage(string message)
		{
			return ProxyServer.FilterDB.Check(message);
		}

		/// <summary>
		/// Check if the host entered is part of the blacklist
		/// </summary>
		/// <param name="host">The host to check</param>
		/// <returns>True if the host is blacklisted, False if it is not</returns>
		private bool FilterHost(string host)
		{
			return ProxyServer.FilterDB.CheckHost(host);
		}
	}
}
